/**
 * HTML5 Canvas-based oscilloscope simulator with responsive rendering, 
 * customizable waveforms, and real-time visualization capabilities.
 * 
 * Features automatic viewport adjustment, debounced rendering optimization,
 * and configurable grid/waveform styling through HTML attributes.
 */

interface Options {
    gridColor: string;
    axisColor: string;
    waveColor: string;
    textColor: string;
    isVertical: boolean;
    fontSize: string;
    fontFamily: string;
    timeAxisDivisions: number;
    amplitudeAxisDivisions: number;
    padding: {
        top: number;
        left: number;
        right: number;
        bottom: number;
    }
}
interface ClientArea {
    top: number;
    left: number;
    right: number;
    bottom: number;
    clientWidth: number;
    clientHeight: number;
    centerX: number;
    centerY: number;
}

/**
 * Type definition for the waveform sampling function, used to generate waveform data displayed on the oscilloscope.
 * This function will be called at each pixel position when drawing the waveform, and calculates the voltage value at the corresponding position based on the input parameters.
 * 
 * @param period - The period of the waveform, in nanoseconds (ns). Represents the time required for the waveform to complete one full cycle.
 * @param periodOffset - The offset of the current time point within one period, in nanoseconds (ns). The value range is `[0, period)`.
 * @param timeOffset - The total time offset, in nanoseconds (ns). Represents the absolute time from the start point of the waveform.
 * @param progress - The horizontal progress of the current pixel in the drawing area, with a value range of `[0, 1]`. 0 represents the leftmost side of the drawing area, and 1 represents the rightmost side.
 * @returns The voltage value at the current position, in millivolts (mV). This value will be used to determine the vertical position of the waveform.
 */
export type WaveformSampler = (period: number, periodOffset: number, timeOffset: number, progress: number) => number;

class Oscilloscope {
    private static readonly DefaultPeakVoltage = 1000; // 1000mV = 1V
    private static readonly DefaultPeriod = 1000;  // 1000ns = 1μs
    private static readonly MinCanvasSize = 10;
    private static readonly PI2 = Math.PI * 2;
    private declare observer: ResizeObserver;
    private canvas: HTMLCanvasElement;
    private ctx: CanvasRenderingContext2D;
    private options: Options;
    private declare clientArea: ClientArea;
    private targetCanvas?: HTMLCanvasElement;
    private targetCtx?: CanvasRenderingContext2D;
    /**
     * A static private method to implement the debounce function. Debounce means that if the same event is triggered multiple times within a certain period, only the last operation will be executed.
     * @typeParam T - The array type of the callback function parameters.
     * @param callback - The callback function to be debounced. The `this` context of this function is an instance of the `Oscilloscope` class.
     * @param delay - The debounce delay time, in milliseconds.
     * @returns Returns a new function that will debounce the input callback function when called.
     */
    private static debounce<T extends any[]>(
        callback: (this: Oscilloscope, ...args: T) => void,
        delay: number
    ): (this: Oscilloscope, ...args: T) => void {
        // Used to store the timer ID returned by setTimeout, initially undefined
        let timeoutId: ReturnType<typeof setTimeout> | undefined;
        // Used to store the arguments of the pending callback function
        let pendingArgs: T;

        return function (this: Oscilloscope, ...args: T) {
            // If new arguments are passed in, update pendingArgs; otherwise, keep it unchanged
            // This ensures that the callback function is executed even if no arguments are passed in during the last call, such as a refresh due to a canvas size change.
            args.length > 0 && (pendingArgs = args);

            // If a timer has been set previously, clear it
            if (timeoutId) clearTimeout(timeoutId);

            // Set a new timer to execute the callback function after the delay time
            timeoutId = setTimeout(() => {
                callback.apply(this, pendingArgs);
                // After executing the callback function, set the timer ID to undefined
                timeoutId = undefined;
            }, delay);
        };
    }
    /**
     * A public method to draw waveforms (with debounce processing).
     * 
     * This method is a debounced drawing method used to draw waveforms on the canvas.
     * Debounce processing ensures that performance issues do not occur during high-frequency calls (such as window resize events).
     * 
     * @param peakVoltage - Optional parameter, peak voltage (unit: mV), default value is {@link Oscilloscope.DefaultPeakVoltage} (1000mV).
     * @param waveformPeriod - Optional parameter, waveform period (unit: ns), default value is {@link Oscilloscope.DefaultPeriod} (1000ns).
     * @param waveformSampler - Optional parameter, waveform sampling function, which receives four parameters:
     *  `peakAmplitude` - Peak amplitude,
     *  `waveformPeriod` - Waveform period,
     *  `timeOffset` - Time offset,
     *  `pixelProgress` - Current pixel drawing progress (between 0 and 1).
     * 
     * @example
     * // Draw a default sine wave (1V peak, 1μs period)
     * oscope.drawWaveform();
     * 
     * // Draw a custom triangle wave (500mV peak, 2000ns period)
     * oscope.drawWaveform(500, 2000, (voltage, period, time, progress) => {
     *   return (time % period) / period * 2 - 1; // Generate a triangle wave
     * });
     * 
     * @see {@link Oscilloscope.debounce} Details of the debounce implementation
     * @see {@link Oscilloscope.drawInternal} Actual drawing logic
     */
    public readonly drawWaveform: (peakAmplitude?: number, waveformPeriod?: number, waveformSampler?: WaveformSampler) => void;

    /**
     * Constructor of the Oscilloscope class, used to initialize an oscilloscope instance.
     * @param canvasElementOrId - Can be an HTMLCanvasElement object or a string representing the ID of the canvas element.
     * @param timeAxisDivisions - Number of divisions on the time axis, default is 14.
     * @param amplitudeAxisDivisions - Number of divisions on the amplitude axis, default is 10.
     */
    constructor(canvasElementOrId: HTMLCanvasElement | string, timeAxisDivisions: number = 14, amplitudeAxisDivisions: number = 10) {
        // Handle the canvas parameter: if it's a string, get the element by ID
        let canvas
        if (typeof canvasElementOrId === 'string') {
            const canvasElement = document.getElementById(canvasElementOrId);
            // Validate that the obtained element is a valid canvas element
            if (!canvasElement || !(canvasElement instanceof HTMLCanvasElement)) {
                throw new Error('Invalid canvas ID');
            }
            canvas = canvasElement;
        } else {
            // If a canvas element is passed in directly, use it
            canvas = canvasElementOrId;
        }

        // Get color configuration from canvas element attributes, use default values if not set
        const gridColor = canvas.getAttribute('grid-color') || '#e0e0e0';
        const axisColor = canvas.getAttribute('axis-color') || '#607D8B';
        const waveColor = canvas.getAttribute('wave-color') || '#FF0000';
        const textColor = canvas.getAttribute('text-color') || '#455A64';
        const rotation = canvas.getAttribute('rotation') || 'horizontal';

        // Get the computed style of the canvas
        const computedStyle = window.getComputedStyle(canvas);
        const fontSize = computedStyle.fontSize;
        const fontFamily = computedStyle.fontFamily;
        const isVertical = rotation === 'vertical';
        // Initialize configuration options
        this.options = {
            gridColor,  // Grid line color
            axisColor,  // Axis color
            waveColor,  // Waveform color
            textColor,  // Text color
            isVertical,
            fontSize,   // Font size
            fontFamily, // Font family
            padding: {  // Padding settings
                top: parseInt(computedStyle.paddingTop, 10) || 2,
                left: parseInt(computedStyle.paddingLeft, 10) || 2,
                right: parseInt(computedStyle.paddingRight, 10) || 2,
                bottom: parseInt(computedStyle.paddingBottom, 10) || 40
            },
            timeAxisDivisions,         // Number of time axis divisions
            amplitudeAxisDivisions     // Number of amplitude axis divisions
        };

        // Get the 2D drawing context
        const ctx = canvas.getContext('2d')!;
        if (!ctx) {
            throw new Error('Failed to get 2D context for canvas');
        }
        if (isVertical) {
            this.targetCanvas = canvas;
            this.targetCtx = ctx;
            this.canvas = document.createElement("canvas");
            this.canvas.width = canvas.height;
            this.canvas.height = canvas.width;
            this.ctx = this.canvas.getContext("2d")!;
        } else {
            this.canvas = canvas;
            this.ctx = ctx;
        }
        // Wrap the drawing method with the debounce function, with a 1ms delay
        this.drawWaveform = Oscilloscope.debounce(this.drawInternal, 1);
        // Initialize the resize observer


        this.initResizeObserver(canvas);
    }

    /**
     * Draw the internal waveform of the oscilloscope.
     * @param peakAmplitude Peak voltage (mV), default is 1000mV (1V).
     * @param waveformPeriod Waveform period (ns), default is 1000ns (1μs).
     * @param waveformSampler Optional waveform sampling function.
     */
    private drawInternal(peakAmplitude: number = Oscilloscope.DefaultPeakVoltage,
        waveformPeriod: number = Oscilloscope.DefaultPeriod,
        waveformSampler?: WaveformSampler) {
        // Check if the canvas area is valid
        if (!this.clientArea ||
            this.clientArea.clientHeight <= Oscilloscope.MinCanvasSize ||
            this.clientArea.clientWidth <= Oscilloscope.MinCanvasSize) {
            return;
        }

        // Parameter validation in development environment
        if (process.env.NODE_ENV === 'development') {
            if (peakAmplitude <= 0 || waveformPeriod <= 0) {
                console.error(`Parameter error: Voltage=${peakAmplitude}mV, Period=${waveformPeriod}ns`);
                return;
            }
        }

        // Ensure parameters are valid, otherwise use default values
        peakAmplitude = peakAmplitude > 0 ? peakAmplitude : Oscilloscope.DefaultPeakVoltage;
        waveformPeriod = waveformPeriod > 0 ? waveformPeriod : Oscilloscope.DefaultPeriod;

        // Destructure to get canvas size and center coordinates
        const { clientWidth, clientHeight, centerX, centerY } = this.clientArea;

        // Get the number of axis divisions from the configuration
        const { timeAxisDivisions, amplitudeAxisDivisions } = this.options;

        // Calculate the time axis and voltage axis ranges
        const timeRange = waveformPeriod * 7;  // Time range of 5 periods
        const voltageRange = peakAmplitude * 2;  // Range of ± peak voltage

        // Calculate the pixel ratios and base values for the time axis and voltage axis
        const { scale: nsPerPixel, valuePerDivision: timePerDivision } = this.calculateBase(timeRange, timeAxisDivisions, clientWidth);
        const { scale: mvPerPixel, valuePerDivision: amplitudePerDivision } = this.calculateBase(voltageRange, amplitudeAxisDivisions, clientHeight);

        // Draw the grid and axes
        this.drawGrid(centerX, centerY, nsPerPixel, mvPerPixel, timePerDivision, amplitudePerDivision);

        // If a waveform sampling function is provided, draw the waveform
        waveformSampler && this.drawWave(peakAmplitude, waveformPeriod, nsPerPixel, mvPerPixel, waveformSampler);

        if (this.targetCtx) {
            this.targetCtx.clearRect(0, 0, this.targetCanvas!.width, this.targetCanvas!.height);
            this.targetCtx.save();
            this.targetCtx.translate(this.targetCanvas!.width / 2, this.targetCanvas!.height / 2);
            this.targetCtx.rotate(Math.PI / 2);
            this.targetCtx.drawImage(this.canvas, -this.canvas.width / 2, -this.canvas.height / 2)
            this.targetCtx.restore();
        }
    }

    /**
     * Draw the waveform on the canvas.
     * @param peakAmplitude Peak voltage (mV).
     * @param waveformPeriod Waveform period (ns).
     * @param nsPerPixel Time axis scale (ns/pixel).
     * @param mvPerPixel Amplitude axis scale (mV/pixel).
     * @param waveformSampler Waveform sampling function.
     */
    private drawWave(
        peakAmplitude: number,
        waveformPeriod: number,
        nsPerPixel: number,
        mvPerPixel: number,
        waveformSampler: WaveformSampler
    ) {
        // Get canvas layout parameters
        const { left, centerX, centerY, clientWidth } = this.clientArea;
        const ctx = this.ctx;

        // Calculate the actual drawing range (from the left padding to the right padding)
        const startX = left;  // Starting X coordinate (left padding)
        const endX = clientWidth + left;  // Ending X coordinate (left padding + available width)

        // Initialize the drawing path
        ctx.beginPath();
        ctx.strokeStyle = this.options.waveColor;  // Set the waveform color
        ctx.lineWidth = 1.5;  // Set the line width

        // Iterate through each horizontal pixel position
        for (let pixelX = startX; pixelX <= endX; pixelX++) {
            // Calculate the horizontal offset of the current pixel relative to the center point
            const xOffset = pixelX - centerX;

            // Convert the pixel offset to a time value (ns)
            const timeOffset = xOffset / nsPerPixel;
            const periodOffset = (timeOffset % waveformPeriod + waveformPeriod) % waveformPeriod;

            // Call the sampling function to get the waveform value and convert it to a vertical coordinate
            const sampleValue = waveformSampler(
                waveformPeriod,
                periodOffset,
                timeOffset,
                (pixelX - startX) / clientWidth  // Current drawing progress (0-1)
            );

            // Calculate the actual Y coordinate of the waveform point (based on the center point)
            const yPos = centerY - (sampleValue * mvPerPixel);

            // Draw the path (use moveTo for the first point, lineTo for subsequent points)
            pixelX === startX ? ctx.moveTo(pixelX, yPos) : ctx.lineTo(pixelX, yPos);
        }

        // Draw the complete path
        ctx.stroke();
    }
    /**
     * Draw the oscilloscope grid and coordinate system.
     * @param centerX Horizontal center coordinate of the canvas (pixels).
     * @param centerY Vertical center coordinate of the canvas (pixels).
     * @param xScale Horizontal scaling factor (pixels/unit).
     * @param yScale Vertical scaling factor (pixels/unit).
     * @param timePerDivision Width of each division on the time axis (nanoseconds/division).
     * @param amplitudePerDivision Height of each division on the amplitude axis (millivolts/division).
     */
    private drawGrid(
        centerX: number,
        centerY: number,
        xScale: number,
        yScale: number,
        timePerDivision: number,
        amplitudePerDivision: number
    ) {
        // Clear the entire canvas area
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

        // Set the grid line style
        this.ctx.strokeStyle = this.options.gridColor;
        this.ctx.lineWidth = 0.7;

        // Draw vertical grid lines (time axis)
        const ticksEachSide = this.options.timeAxisDivisions >> 1; // Calculate the number of grid lines to draw on each side
        this.ctx.beginPath();
        // Draw grid lines from the center to both sides
        for (let divisionIndex = -ticksEachSide; divisionIndex <= ticksEachSide; divisionIndex++) {
            // Calculate the X coordinate of the current grid line: center point + number of divisions * width per division * scaling factor
            const x = centerX + divisionIndex * timePerDivision * xScale;
            // Draw a vertical line from the top to the bottom
            this.ctx.moveTo(x, this.options.padding.top);
            this.ctx.lineTo(x, this.canvas.height - this.options.padding.bottom);
        }

        // Draw horizontal grid lines (amplitude axis)
        const amplitudeTicksEachSide = this.options.amplitudeAxisDivisions >> 1;
        // Draw grid lines from the center to the top and bottom
        for (let divisionIndex = -amplitudeTicksEachSide; divisionIndex <= amplitudeTicksEachSide; divisionIndex++) {
            // Calculate the Y coordinate of the current grid line: center point - number of divisions * height per division * scaling factor
            const y = centerY - divisionIndex * amplitudePerDivision * yScale;
            // Draw a horizontal line from left to right
            this.ctx.moveTo(this.options.padding.left, y);
            this.ctx.lineTo(this.canvas.width - this.options.padding.right, y);
        }
        this.ctx.stroke();

        // Draw the center reference lines (using the axis color)
        this.ctx.strokeStyle = this.options.axisColor;
        this.ctx.lineWidth = 1;

        // Draw the vertical center line (X axis)
        this.ctx.beginPath();
        this.ctx.moveTo(centerX, this.options.padding.top);
        this.ctx.lineTo(centerX, this.canvas.height - this.options.padding.bottom);

        // Draw the horizontal center line (Y axis)
        this.ctx.moveTo(this.options.padding.left, centerY);
        this.ctx.lineTo(this.canvas.width - this.options.padding.right, centerY);
        this.ctx.stroke();

        // Convert the time unit to a more readable format (ms/μs/ns)
        const timeInfo = this.convertTimeUnit(timePerDivision);
        // Format the amplitude value: round to an integer if greater than 100, keep one decimal place if less than 100
        const ampValue = amplitudePerDivision.toFixed(amplitudePerDivision >= 100 ? 0 : 1);

        // Set the label position (bottom left, 10px from the left, 5px from the bottom)
        const labelX = this.options.padding.left + 10;
        const labelY = this.canvas.height - this.options.padding.bottom + 5;

        // Draw the amplitude axis label (using the waveform color)
        this.ctx.textAlign = 'left';
        this.ctx.textBaseline = 'top';
        this.ctx.fillStyle = this.options.waveColor;
        this.ctx.fillText(`Amplitude Axis: ${ampValue}mV/division`, labelX, labelY);

        // Draw the time axis label (using the text color)
        this.ctx.fillStyle = this.options.textColor;
        this.ctx.fillText(`Time Axis: ${timeInfo.value}${timeInfo.unit}/division`, labelX, labelY + 20);
    }

    /**
     * Callback function to handle canvas size changes.
     * @param newWidth New canvas width (pixels).
     * @param newHeight New canvas height (pixels).
     */
    private handleResize(newWidth: number, newHeight: number) {
        // Update the actual size of the canvas
        this.canvas.width = newWidth;
        this.canvas.height = newHeight;

        // Set the canvas text style (using the font settings from the configuration)
        this.ctx.font = `${this.options.fontSize} ${this.options.fontFamily}`;

        // Destructure to get the padding configuration
        const { top, left, right, bottom } = this.options.padding;

        // Calculate the actual drawable area size (minus the padding)
        const clientWidth = newWidth - left - right;
        const clientHeight = newHeight - top - bottom;

        // Update the client area information
        this.clientArea = {
            top,        // Top padding
            left,       // Left padding
            right,      // Right padding
            bottom,     // Bottom padding
            clientWidth,  // Available width
            clientHeight, // Available height
            centerX: clientWidth / 2 + left,  // Horizontal center point (X coordinate)
            centerY: clientHeight / 2 + top   // Vertical center point (Y coordinate)
        }

        // Trigger waveform redraw when the available area is valid (width and height > 0)
        if (clientWidth > Oscilloscope.MinCanvasSize &&
            clientHeight > Oscilloscope.MinCanvasSize)
            this.drawWaveform();
    }

    /**
     * Initialize the resize observer.
     * Determine the observation target and calculation method based on whether the canvas fills the parent container.
     */
    private initResizeObserver(canvas: HTMLCanvasElement) {
        // Check if the canvas is set to 100% width and height (fills the parent container)
        const isCanvasFullSize = canvas.style.width === "100%" && canvas.style.height === "100%";

        // Determine the resize target: if it's full size, observe the canvas itself; otherwise, observe the parent element
        const resizeTarget = isCanvasFullSize ? canvas : canvas.parentElement;

        // Ensure the target element exists
        if (!resizeTarget) throw new Error('Canvas must have a parent element');

        // Create a debounced resize handler
        const handleResize = Oscilloscope.debounce(this.handleResize, 100);

        // Handle non-full size mode
        if (!isCanvasFullSize) {
            this.observer = new ResizeObserver(entries => {
                // Get the computed style of the target element
                const computedStyle = window.getComputedStyle(resizeTarget);
                // Calculate the total horizontal padding
                const paddingHorizontal = parseFloat(computedStyle.paddingLeft) + parseFloat(computedStyle.paddingRight);
                // Calculate the total vertical padding
                const paddingVertical = parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
                // Call the handler function with the actual size minus the padding
                canvas.width = resizeTarget.clientWidth - paddingHorizontal;
                canvas.height = resizeTarget.clientHeight - paddingVertical;
                this.options.isVertical ?
                    handleResize.call(this, canvas.height, canvas.width) :
                    handleResize.call(this, canvas.width, canvas.height);
            })
        }
        // Handle full size mode
        else {
            this.observer = new ResizeObserver(entries => {
                for (const entry of entries) {
                    if (entry.contentBoxSize) {
                        // Handle differences in browser APIs
                        const contentSize: ResizeObserverSize = Array.isArray(entry.contentBoxSize)
                            ? entry.contentBoxSize[0]
                            : entry.contentBoxSize;

                        canvas.width = contentSize.inlineSize;
                        canvas.height = contentSize.blockSize;
                        // Use the content box size directly
                        this.options.isVertical ?
                            handleResize.call(this, canvas.height, canvas.width) :
                            handleResize.call(this, canvas.width, canvas.height);
                    }
                }
            })
        }
        // Start observing the target element
        this.observer.observe(resizeTarget);
    }

    /**
     * Calculate the axis base value and scaling ratio.
     * @param totalRange Total range value (time range in ns or voltage range in mV).
     * @param divisionCount Number of axis divisions.
     * @param availablePixels Available pixel size.
     * @returns An object containing the scaling ratio and base value per division.
     */
    private calculateBase(totalRange: number, divisionCount: number, availablePixels: number) {
        // Calculate the initial base value per division
        const initialDivisionValue = totalRange / divisionCount;
        // Calculate the order of magnitude (power of 10)
        const magnitude = Math.pow(10, Math.floor(Math.log10(initialDivisionValue)));
        // Normalize the value
        const normalizedValue = initialDivisionValue / magnitude;
        // Select an appropriate base factor from the standard coefficients
        const baseFactor = [1, 2, 5, 10].find(factor => normalizedValue <= factor)!;

        return {
            scale: availablePixels / (baseFactor * magnitude * divisionCount), // Pixel/unit value ratio
            valuePerDivision: baseFactor * magnitude, // Actual value represented by each division
        }
    }
    /**
     * Destroy resources related to the oscilloscope instance.
     * This method is used to clean up resources related to the oscilloscope instance, mainly disconnect the ResizeObserver listener and clear the drawing context.
     * After calling this method, the oscilloscope will no longer respond to window size change events and cannot continue drawing operations.
     */
    dispose() {
        // Check if the ResizeObserver instance exists
        if (this.observer) {
            // Disconnect the ResizeObserver listener, stop observing the target element's size changes
            this.observer.disconnect();
            // Set the ResizeObserver instance to undefined to release the reference
            this.observer = undefined!;
        }
        if (this.targetCanvas) {
            this.targetCanvas = undefined!;
            this.targetCtx = undefined!
        }
        this.canvas = undefined!;
        this.ctx = undefined!
        // Set the drawing context to undefined to release the reference
    }
    /**
     * Convert the time value to a more readable unit representation.
     * @param time Time value, in nanoseconds.
     * @returns An object containing the converted time value and the corresponding time unit.
     */
    private convertTimeUnit(time: number) {
        if (time >= 1e6) return { value: time / 1e6, unit: 'ms' };
        if (time >= 1e3) return { value: time / 1e3, unit: 'μs' };
        return { value: time, unit: 'ns' };
    }
    /**
     * Draw a sine waveform.
     * @param peakToPeak Peak-to-peak voltage (mV), the voltage difference between the highest and lowest points of the waveform.
     * @param waveformPeriod Waveform period (ns).
     */
    drawSinWave(peakToPeak: number, waveformPeriod: number) {
        // Calculate the half amplitude (half of the peak-to-peak value)
        const halfPeak = peakToPeak / 2;
        this.drawWaveform(halfPeak, waveformPeriod, (period, periodOffset) => {
            return (Math.sin(periodOffset / period * Oscilloscope.PI2) * halfPeak);
        });
    }
    /**
     * Draw a frequency-swept sine wave.
     * @param peakToPeak Peak-to-peak voltage (mV).
     * @param startFreq Starting frequency (Hz).
     * @param endFreq Ending frequency (Hz).
     * @param bidirectional Whether to perform bidirectional scanning.
     */
    public drawSineSweepWave(peakToPeak: number, startFreq: number, endFreq: number, bidirectional: boolean = false) {
        // Parameter validation (clearer error messages)
        if (peakToPeak <= 0 || startFreq <= 0 || endFreq <= 0) {
            throw new Error(`Parameters must be positive numbers, received: peakToPeak=${peakToPeak}, startFreq=${startFreq}, endFreq=${endFreq}`);
        }

        // Use an arrow function to bind the this context (improve code stability)
        const createWave = (): WaveformSampler => {
            const halfAmplitude = peakToPeak / 2;
            const freqScale = 1e-9; // Hz -> 1/ns conversion factor

            // Pre-calculate frequency parameters (improve readability)
            const start = startFreq * freqScale;
            const delta = (endFreq * freqScale) - start;

            let accumulatedPhase = 0;
            let previousTime = 0;
            return (_, __, timeOffset, progress) => {
                // Use the cached progress to calculate the current frequency (reduce redundant calculations)
                const effectiveProgress = bidirectional
                    ? progress < 0.5
                        ? progress * 2
                        : 2 - progress * 2
                    : progress;
                const currentFreq = start + delta * effectiveProgress;
                // Optimize phase integration calculation (avoid floating-point error accumulation)
                if (previousTime !== 0) {
                    const deltaTime = timeOffset - previousTime;
                    accumulatedPhase += (currentFreq * deltaTime) * Oscilloscope.PI2;
                }
                previousTime = timeOffset;
                return Math.sin(accumulatedPhase) * halfAmplitude;
            };
        };

        // Use default period parameters (the actual period is dynamically determined by the sweep logic)
        this.drawWaveform(peakToPeak / 2, 1e9 / Math.min(startFreq, endFreq), createWave());
    }


    /**
     * Draw a unipolar pulse waveform.
     * @param peakAmplitude Pulse peak amplitude (mV).
     * @param waveformPeriod Waveform period (ns).
     * @param pulseWidth Pulse width (ns).
     */
    drawPulseWave(peakToPeak: number, waveformPeriod: number, pulseWidth: number) {
        // Parameter validation
        if ([peakToPeak, waveformPeriod, pulseWidth].some(v => v <= 0)) {
            throw new Error('Parameters must be positive numbers');
        }
        if (pulseWidth > waveformPeriod) {
            throw new Error('Pulse width cannot exceed the period');
        }

        // Pre-calculate the average amplitude (optimize performance)
        const avgAmplitude = peakToPeak * (pulseWidth / waveformPeriod);
        const actualPeek = Math.max(peakToPeak - avgAmplitude, avgAmplitude)
        this.drawWaveform(actualPeek, waveformPeriod, (_, periodOffset) => {
            // Check if it's within the pulse interval
            return periodOffset < pulseWidth
                ? peakToPeak - avgAmplitude  // Positive pulse part
                : -avgAmplitude;             // Negative level part
        });
    }
    /**
    * Draw a bipolar pulse waveform.
    * @param peakToPeak Peak-to-peak voltage (mV), the voltage difference between the highest and lowest points of the waveform.
    * @param waveformPeriod Waveform period (ns).
    * @param pulseWidth Pulse width (ns).
    */
    drawBipolarPulse(peakToPeak: number, waveformPeriod: number, pulseWidth: number) {
        // Parameter validation
        if ([peakToPeak, waveformPeriod, pulseWidth].some(v => v <= 0)) {
            throw new Error('Parameters must be positive numbers');
        }
        if (pulseWidth > waveformPeriod / 2) {
            throw new Error('Pulse width cannot exceed half of the period');
        }

        // Calculate the half amplitude (half of the peak-to-peak value)
        const halfPeak = peakToPeak / 2;
        const halfPeriod = waveformPeriod / 2;
        this.drawWaveform(halfPeak, waveformPeriod, (_, periodOffset, timeOffset) => {
            // Check if it's within the positive pulse interval
            if (periodOffset < pulseWidth) return halfPeak;
            // Check if it's within the negative pulse interval
            if (periodOffset > halfPeriod && periodOffset < halfPeriod + pulseWidth) return -halfPeak;
            // Return 0 at other times
            return 0;
        });
    }
    /**
     * Draw a triangle waveform.
     * @param peakToPeak - Peak-to-peak voltage (mV), the voltage difference between the highest and lowest points of the waveform.
     * @param waveformPeriod - Waveform period (ns), the time required to complete one full waveform.
     * @param pulseWidth - Rise time (ns), the time required to rise from the trough to the peak.
     */
    public drawTriangleWave(
        peakToPeak: number,
        waveformPeriod: number,
        pulseWidth: number
    ) {
        // Parameter validation: ensure all input values are positive
        if ([peakToPeak, waveformPeriod].some(v => v <= 0)) {
            throw new Error('Parameters must be greater than zero');
        }

        // Calculate the half amplitude (half of the peak-to-peak value)
        const halfPeak = peakToPeak / 2;

        // Calculate the slopes: rise slope = peak-to-peak value / rise time, fall slope = peak-to-peak value / fall time
        const slopeUp = peakToPeak / pulseWidth;
        const slopeDown = peakToPeak / (waveformPeriod - pulseWidth);

        // Call the core drawing function with the triangle wave generator
        this.drawWaveform(halfPeak, waveformPeriod,
            (_, periodOffset) => {

                // Return the waveform value based on the phase position:
                // Rising phase: linearly increase from -halfPeak to +halfPeak
                // Falling phase: linearly decrease from +halfPeak to -halfPeak
                return periodOffset < pulseWidth ?
                    -halfPeak + slopeUp * periodOffset :
                    halfPeak - slopeDown * (periodOffset - pulseWidth);
            }
        );
    }
}

export { Oscilloscope };
